/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Net.Sockets;
using System.IO;
using System.Text;
using System.Threading;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using EffiProz.Core;
using EffiProz.Core.Lib;
using EffiProz.Core.Lib.IO;
using EffiProz.Core.RowIO;
using EffiProz.Core.Results;
using EffiProz.Core.Errors;

// fredt@users 20020215 - patch 461556 by paul-h@userus - server factory
// fredt@users 20020424 - patch 1.7.0 by fredt - shutdown without exit
// fredt@users 20021002 - patch 1.7.1 by fredt - changed notification method
// fredt@users 20030618 - patch 1.7.2 by fredt - changed read/write methods

/**
 *  All ServerConnection objects are listed _in a Set _in server
 *  and removed by this class when closed.<p>
 *
 *  When the database or server is shutdown, the signalClose() method is called
 *  for all current ServerConnection instances. This will call the private
 *  close() method unless the ServerConnection thread itself has caused the
 *  shutdown. In this case, the keepAlive flag is set to false, allowing the
 *  thread to terminate once it has returned the result of the operation to
 *  the client.
 *  (fredt@users)<p>
 *
 * Rewritten _in version HSQLDB 1.7.2, based on original Hypersonic code.
 *
 * @author Thomas Mueller (Hypersonic SQL Group)
 * @author fredt@users
 * @version 1.8.0
 * @since Hypersonic SQL
 */

namespace EffiProz.Server
{

    public class ServerConnection
    {

        public bool keepAlive;
        private string user;      
        public int dbID;
        public int dbIndex;
        private volatile Session session;
        private TcpClient listner;
        private Server server;
        private DataInputStream dataInput;
        private DataOutputStream dataOutput;
        private static int mCurrentThread = 0;
        private int mThread;
        public const int BUFFER_SIZE = 0x1000;
        public byte[] mainBuffer = new byte[BUFFER_SIZE];
        public RowOutputInterface rowOut;
        public RowInputBinary rowIn;
        public Thread runnerThread;
        protected static String TEXTBANNER_PART1 = null;
        protected static String TEXTBANNER_PART2 = null;

        /**
         * Creates a new ServerConnection to the specified Server on the
         * specified socket.
         *
         * @param socket the network socket on which Server communication
         *      takes place
         * @param server the Server instance to which the object
         *      represents a connection
         */
        public ServerConnection(TcpClient listner, Server server)
        {
            RowOutputBinary rowOutTemp = new RowOutputBinary(mainBuffer);

            rowIn = new RowInputBinary(rowOutTemp);
            rowOut = rowOutTemp;

            this.listner = listner;
            this.server = server;

            lock (typeof(ServerConnection))
            {
                mThread = mCurrentThread++;
            }

            lock (server.serverConnSet)
            {
                server.serverConnSet.Add(this);
            }
        }

        /**
         * Signals this object to close, including exiting the thread running
         * the request handling loop
         */
        public void signalClose()
        {
            keepAlive = false;

            if (!Thread.CurrentThread.Equals(runnerThread))
            {
                close();
            }
        }

        /**
         * Closes this connection.
         */
        private void close()
        {

            if (session != null)
            {
                session.close();

                session = null;
            }

            // fredt@user - closing the socket is to stop this thread
            try
            {
                listner.Close();
            }
            catch (Exception) { }

            listner = null;

            lock (server.serverConnSet)
            {
                server.serverConnSet.Remove(this);
            }
        }
        
        internal static Stream GetServerStream(bool isTLS, TcpClient client,X509Certificate serverCertificate)
        {
            if (isTLS)
            {
                SslStream sslStream = new SslStream(
                            client.GetStream(), false);

                sslStream.AuthenticateAsServer(serverCertificate,
                  false, SslProtocols.Tls, false);

                return sslStream;

            }
            else
            {
                return client.GetStream();
            }
        }

        /**
         * Initializes this connection.
         */
        private void init()
        {

            runnerThread = Thread.CurrentThread;
            keepAlive = true;

            try
            {
                listner.NoDelay = true;

                Stream serverStream = GetServerStream(server.isTls(), listner, server.ServerCertificate);

                dataOutput = new DataOutputStream(serverStream);
                dataInput = new DataInputStream(serverStream);
    

                Result resultIn = Result.newResult(dataInput, rowIn);
                resultIn.readAdditionalResults(session, dataInput, rowIn);

                Result resultOut;

                resultOut = setDatabase(resultIn);

                resultOut.write(dataOutput, rowOut);

                return;
            }
            catch (Exception e)
            {
                StringBuilder sb = new StringBuilder(mThread
                                              + ":Failed to connect client.");

                if (user != null)
                {
                    sb.Append("  User '" + user + "'.");
                }

                server.printWithThread(sb.ToString() + "  Stack trace follows.");
                server.printStackTrace(e);
            }
        }

        /**
         * Initializes this connection and runs the request handling
         * loop until closed.
         */
        public void run()
        {
            int msgType;
            init();

            if (session != null)
            {
                try
                {
                    while (keepAlive)
                    {
                        if (!listner.Connected)
                            break;
                        if (listner.Available == 0)
                        {
                            Thread.Sleep(10);
                            continue;
                        }

                        msgType = dataInput.ReadByte();
                        receiveResult(msgType);

                    }
                }
                catch (CleanExit )
                {
                    keepAlive = false;
                }
                catch (IOException)
                {
                    //   fredt - is thrown when connection drops
                    server.printWithThread(mThread + ":disconnected " + user);
                }
                catch (CoreException e)
                {
                    //  fredt - is thrown while constructing the result
                    if (keepAlive)
                        server.printStackTrace(e);
                }
            }

            close();
        }

        private class CleanExit : Exception {}

        private CleanExit cleanExit = new CleanExit();

        private void receiveResult(int resultMode)
        {

            Result resultIn = Result.newResult(session, resultMode, dataInput,
                                               rowIn);

            resultIn.readAdditionalResults(session, dataInput, rowIn);
            server.printRequest(mThread, resultIn);

            Result resultOut = null;

            switch (resultMode)
            {

                case ResultConstants.CONNECT:
                    {
                        resultOut = setDatabase(resultIn);

                        break;
                    }
                case ResultConstants.DISCONNECT:
                    {
                        throw cleanExit;
                    }
                case ResultConstants.RESETSESSION:
                    {
                        resetSession();

                        return;
                    }
                default:
                    resultOut = session.execute(resultIn);
                    break;
            }

            resultOut.write(dataOutput, rowOut);

            if (resultOut.getNavigator() != null)
            {
                resultOut.getNavigator().close();
            }

            rowOut.setBuffer(mainBuffer);
            rowIn.resetRow(mainBuffer.Length);
        }


        private Result setDatabase(Result resultIn)
        {

            try
            {
                String databaseName = resultIn.getDatabaseName();

                dbIndex = server.getDBIndex(databaseName);
                dbID = server.dbID[dbIndex];
                user = resultIn.getMainString();

                if (!server.isSilent())
                {
                    server.printWithThread(mThread + ":Trying to connect user '"
                                           + user + "' to DB (" + databaseName
                                           + ')');
                }

                session = DatabaseManager.newSession(dbID, user,
                                                     resultIn.getSubString(),
                                                     resultIn.getZoneString(),
                                                     resultIn.getUpdateCount());

                if (!server.isSilent())
                {
                    server.printWithThread(mThread + ":Connected user '" + user
                                           + "'");
                }

                return Result.newConnectionAcknowledgeResponse(session.getId(),
                        session.getDatabase().getDatabaseID());
            }
            catch (CoreException e)
            {
                session = null;

                return Result.newErrorResult(e);
            }
            catch (Exception e)
            {
                session = null;

                return Result.newErrorResult(e);
            }
        }

        /**
         * Used by pooled connections to close the existing SQL session and open
         * a new one.
         */
        private void resetSession()
        {
            session.close();            
        }

        /**
         * Retrieves the thread name to be used  when
         * this object is the Runnable object of a Thread.
         *
         * @return the thread name to be used  when this object is the Runnable
         * object of a Thread.
         */
        public string getConnectionThreadName()
        {
            return "EffiProzDB Connection @" + string.Format("{0:x2}", this.GetHashCode());
        }
    }
}
